Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: A Gradle Build Cache server with Redis backend for Theia IDE deploy
type: application

# Chart version - bump for breaking changes
version: 0.3.0
version: 0.3.1

# Application version - matches the cache server version
appVersion: "0.1.0"
Expand Down
27 changes: 27 additions & 0 deletions chart/templates/auth-secrets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{- if and .Values.enabled .Values.auth.enabled }}
# Read-only cache credentials (for Theia IDE / students)
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-cache-reader
labels:
app: {{ .Release.Name }}
role: reader
type: kubernetes.io/basic-auth
data:
username: {{ .Values.auth.reader.username | b64enc | quote }}
password: {{ .Values.auth.reader.password | b64enc | quote }}
---
# Read-write cache credentials (for CI/CD, admin, cache pre-warming)
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-cache-writer
labels:
app: {{ .Release.Name }}
role: writer
type: kubernetes.io/basic-auth
data:
username: {{ .Values.auth.writer.username | b64enc | quote }}
password: {{ .Values.auth.writer.password | b64enc | quote }}
{{- end }}
6 changes: 4 additions & 2 deletions chart/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ data:
auth:
enabled: {{ .Values.auth.enabled }}
{{- if .Values.auth.enabled }}
users:
- username: {{ .Values.auth.username | quote }}
reader:
username: {{ .Values.auth.reader.username | quote }}
writer:
username: {{ .Values.auth.writer.username | quote }}
{{- end }}

logging:
Expand Down
11 changes: 8 additions & 3 deletions chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ spec:
name: {{ .Release.Name }}-redis-secret
key: redis-password
{{- if .Values.auth.enabled }}
- name: CACHE_PASSWORD
- name: CACHE_READER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redis-secret
key: cache-password
name: {{ .Release.Name }}-cache-reader
key: password
- name: CACHE_WRITER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-cache-writer
key: password
{{- end }}
args:
- --config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ data:
{{- else }}
redis-password: {{ randAlphaNum 32 | b64enc}}
{{- end }}
cache-password: {{ .Values.auth.password | b64enc | quote}}
{{- end }}
17 changes: 13 additions & 4 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ image:
tag: "main"
pullPolicy: IfNotPresent

# Authentication credentials
# Authentication credentials (role-based)
# reader: read-only access (for Theia IDE / students)
# writer: read-write access (for CI/CD, admin, cache pre-warming)
auth:
enabled: true
username: "gradle"
# IMPORTANT: Change this password in production!
password: "changeme"
reader:
username: "reader"
# IMPORTANT: Change this password in production!
# optimaly from github envrionment secrets
password: "changeme-reader"
writer:
username: "writer"
# IMPORTANT: Change this password in production!
# optimaly from github envrionment secrets
password: "changeme-writer"

# Resource limits
resources:
Expand Down
29 changes: 11 additions & 18 deletions src/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ type CacheConfig struct {
}

type AuthConfig struct {
Enabled bool `mapstructure:"enabled"`
Users []UserAuth `mapstructure:"users"`
Enabled bool `mapstructure:"enabled"`
Reader UserAuth `mapstructure:"reader"`
Writer UserAuth `mapstructure:"writer"`
}

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

v.BindEnv("auth.users.0.password", "CACHE_PASSWORD")
v.BindEnv("auth.reader.password", "CACHE_READER_PASSWORD")
v.BindEnv("auth.writer.password", "CACHE_WRITER_PASSWORD")

v.BindEnv("sentry.dsn", "SENTRY_DSN")

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

// Handle CACHE_PASSWORD environment variable for default user
if cachePassword := v.GetString("CACHE_PASSWORD"); cachePassword != "" {
if len(cfg.Auth.Users) > 0 {
cfg.Auth.Users[0].Password = cachePassword
}
}

return &cfg, nil
}

func (c *Config) Validate() error {
if c.Storage.Addr == "" {
return fmt.Errorf("storage.addr is required")
}
if c.Auth.Enabled && len(c.Auth.Users) == 0 {
return fmt.Errorf("auth.users is required when auth is enabled")
}
for i, user := range c.Auth.Users {
if user.Username == "" {
return fmt.Errorf("auth.users[%d].username is required", i)
if c.Auth.Enabled {
if c.Auth.Reader.Username == "" || c.Auth.Reader.Password == "" {
return fmt.Errorf("auth.reader.username and auth.reader.password are required when auth is enabled")
}
if user.Password == "" {
return fmt.Errorf("auth.users[%d].password is required", i)
if c.Auth.Writer.Username == "" || c.Auth.Writer.Password == "" {
return fmt.Errorf("auth.writer.username and auth.writer.password are required when auth is enabled")
}
}
if c.Server.TLS.Enabled {
Expand Down
26 changes: 11 additions & 15 deletions src/internal/middleware/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@ import (
"github.com/kevingruber/gradle-cache/internal/config"
)

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

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

expectedPassword, userExists := credentials[username]
if !userExists {
// Check credentials
isReader := username == auth.Reader.Username &&
subtle.ConstantTimeCompare([]byte(password), []byte(auth.Reader.Password)) == 1
isWriter := username == auth.Writer.Username &&
subtle.ConstantTimeCompare([]byte(password), []byte(auth.Writer.Password)) == 1

if !isReader && !isWriter {
c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`)
c.AbortWithStatus(http.StatusUnauthorized)
return
}

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

// Store username in context for logging/metrics
c.Set("username", username)
c.Next()
}
}
18 changes: 12 additions & 6 deletions src/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,19 @@ func (s *Server) setupRoutes() {

// Create cache group with optional auth
cacheGroup := s.router.Group("/cache")
if s.cfg.Auth.Enabled {
cacheGroup.Use(middleware.BasicAuth(s.cfg.Auth.Users))
}

cacheGroup.GET("/:key", cacheHandler.Get)
cacheGroup.PUT("/:key", cacheHandler.Put)
cacheGroup.HEAD("/:key", cacheHandler.Head)
cacheGroup.GET("/:key", s.cacheAuth(false), cacheHandler.Get)
cacheGroup.HEAD("/:key", s.cacheAuth(false), cacheHandler.Head)
cacheGroup.PUT("/:key", s.cacheAuth(true), cacheHandler.Put)
}

func (s *Server) cacheAuth(requireWriter bool) gin.HandlerFunc {
if !s.cfg.Auth.Enabled {
return func(c *gin.Context) {
c.Next()
}
}
return middleware.CacheAuth(s.cfg.Auth, requireWriter)
}

// handlePing is a simple health check endpoint.
Expand Down