From 95411857799b16ec550dd3e2487a0b8eec6f9302 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 10 Feb 2026 17:57:58 +0100 Subject: [PATCH 1/3] Switch to reader and writer auth --- chart/Chart.yaml | 2 +- chart/templates/configmap.yaml | 7 +++++-- chart/templates/deployment.yaml | 29 ++++++++++++++++++++------- chart/templates/secrets.yaml | 34 ++++++++++++++++++++++++++++---- chart/values.yaml | 19 +++++++++++++----- src/internal/config/config.go | 26 +++++++++++++++++------- src/internal/middleware/auth.go | 35 +++++++++++++++++++++++++++------ src/internal/server/server.go | 9 ++++++++- 8 files changed, 128 insertions(+), 33 deletions(-) diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 5d79bf9..54fe403 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -5,7 +5,7 @@ description: A Gradle Build Cache server with MinIO backend for Theia IDE deploy type: application # Chart version - bump for breaking changes -version: 0.2.3 +version: 0.2.4 # Application version - matches the cache server version appVersion: "0.1.0" diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index 0aee82d..d04612b 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -14,7 +14,7 @@ data: write_timeout: 120s {{- if .Values.tls.enabled }} tls: - enabled: true + enabled: true cert_file: /etc/certs/tls.crt key_file: /etc/certs/tls.key {{- end}} @@ -31,7 +31,10 @@ data: enabled: {{ .Values.auth.enabled }} {{- if .Values.auth.enabled }} users: - - username: {{ .Values.auth.username | quote }} + - username: {{ .Values.auth.reader.username | quote }} + role: "reader" + - username: {{ .Values.auth.writer.username | quote }} + role: "writer" {{- end }} logging: diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 34e78d8..bfe3e9e 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -47,12 +47,12 @@ spec: - name: MINIO_ACCESS_KEY valueFrom: secretKeyRef: - name: {{ .Release.Name }}-secrets + name: {{ .Release.Name }}-minio-secrets key: minio-access-key - name: MINIO_SECRET_KEY valueFrom: secretKeyRef: - name: {{ .Release.Name }}-secrets + name: {{ .Release.Name }}-minio-secrets key: minio-secret-key containers: - name: cache-server @@ -66,19 +66,34 @@ spec: - name: MINIO_ACCESS_KEY valueFrom: secretKeyRef: - name: {{ .Release.Name }}-secrets + name: {{ .Release.Name }}-minio-secrets key: minio-access-key - name: MINIO_SECRET_KEY valueFrom: secretKeyRef: - name: {{ .Release.Name }}-secrets + name: {{ .Release.Name }}-minio-secrets key: minio-secret-key {{- if .Values.auth.enabled }} - - name: CACHE_PASSWORD + - name: CACHE_READER_USERNAME valueFrom: secretKeyRef: - name: {{ .Release.Name }}-secrets - key: cache-password + name: {{ .Release.Name }}-cache-reader + key: username + - name: CACHE_READER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-cache-reader + key: password + - name: CACHE_WRITER_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-cache-writer + key: username + - name: CACHE_WRITER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-cache-writer + key: password {{- end }} args: - --config diff --git a/chart/templates/secrets.yaml b/chart/templates/secrets.yaml index 3fbee47..324aa85 100644 --- a/chart/templates/secrets.yaml +++ b/chart/templates/secrets.yaml @@ -1,15 +1,41 @@ {{- if .Values.enabled }} +# MinIO storage credentials apiVersion: v1 kind: Secret metadata: - name: {{ .Release.Name }}-secrets + name: {{ .Release.Name }}-minio-secrets labels: app: {{ .Release.Name }} type: Opaque data: minio-access-key: {{ "minioadmin" | b64enc | quote }} minio-secret-key: {{ "minioadmin" | b64enc | quote }} - {{- if .Values.auth.enabled }} - cache-password: {{ .Values.auth.password | b64enc | quote }} - {{- end }} +{{- if .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 }} {{- end }} diff --git a/chart/values.yaml b/chart/values.yaml index 977f21f..5b2b22f 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -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" # Storage configuration storage: @@ -99,4 +108,4 @@ reposilite: subPath: configuration.shared.json readOnly: true - name: plugins - mountPath: /app/data/plugins \ No newline at end of file + mountPath: /app/data/plugins diff --git a/src/internal/config/config.go b/src/internal/config/config.go index cd4128c..9cb01e3 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "strings" "time" @@ -51,6 +52,7 @@ type AuthConfig struct { type UserAuth struct { Username string `mapstructure:"username"` Password string `mapstructure:"password"` + Role string `mapstructure:"role"` } type MetricsConfig struct { @@ -108,7 +110,6 @@ func Load(configPath string) (*Config, error) { // Bind specific environment variables v.BindEnv("storage.access_key", "MINIO_ACCESS_KEY") v.BindEnv("storage.secret_key", "MINIO_SECRET_KEY") - v.BindEnv("auth.users.0.password", "CACHE_PASSWORD") v.BindEnv("sentry.dsn", "SENTRY_DSN") var cfg Config @@ -116,16 +117,27 @@ func Load(configPath string) (*Config, error) { 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 - } - } + // Override user credentials from environment variables if set + overrideUserFromEnv(&cfg, 0, "CACHE_READER_USERNAME", "CACHE_READER_PASSWORD") + overrideUserFromEnv(&cfg, 1, "CACHE_WRITER_USERNAME", "CACHE_WRITER_PASSWORD") return &cfg, nil } +// overrideUserFromEnv overrides username and password for a specific user index +// from environment variables, if they are set. +func overrideUserFromEnv(cfg *Config, index int, usernameEnv, passwordEnv string) { + if index >= len(cfg.Auth.Users) { + return + } + if val := os.Getenv(usernameEnv); val != "" { + cfg.Auth.Users[index].Username = val + } + if val := os.Getenv(passwordEnv); val != "" { + cfg.Auth.Users[index].Password = val + } +} + func (c *Config) Validate() error { if c.Storage.Endpoint == "" { return fmt.Errorf("storage.endpoint is required") diff --git a/src/internal/middleware/auth.go b/src/internal/middleware/auth.go index 2c63288..d2f667e 100644 --- a/src/internal/middleware/auth.go +++ b/src/internal/middleware/auth.go @@ -8,12 +8,21 @@ import ( "github.com/kevingruber/gradle-cache/internal/config" ) -// BasicAuth creates a middleware that validates HTTP Basic Authentication. +type userCredential struct { + Password string + Role string +} + +// BasicAuth creates a middleware that validates HTTP Basic Authentication +// and stores the user's role in the gin context. func BasicAuth(users []config.UserAuth) gin.HandlerFunc { // Build a map for O(1) lookup - credentials := make(map[string]string, len(users)) + credentials := make(map[string]userCredential, len(users)) for _, user := range users { - credentials[user.Username] = user.Password + credentials[user.Username] = userCredential{ + Password: user.Password, + Role: user.Role, + } } return func(c *gin.Context) { @@ -24,7 +33,7 @@ func BasicAuth(users []config.UserAuth) gin.HandlerFunc { return } - expectedPassword, userExists := credentials[username] + cred, userExists := credentials[username] if !userExists { c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) c.AbortWithStatus(http.StatusUnauthorized) @@ -32,14 +41,28 @@ func BasicAuth(users []config.UserAuth) gin.HandlerFunc { } // Use constant-time comparison to prevent timing attacks - if subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) != 1 { + if subtle.ConstantTimeCompare([]byte(password), []byte(cred.Password)) != 1 { c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) c.AbortWithStatus(http.StatusUnauthorized) return } - // Store username in context for logging/metrics + // Store username and role in context for logging/metrics and authorization c.Set("username", username) + c.Set("role", cred.Role) + c.Next() + } +} + +// RequireRole creates a middleware that checks if the authenticated user +// has the required role. Returns 403 Forbidden if not. +func RequireRole(requiredRole string) gin.HandlerFunc { + return func(c *gin.Context) { + role, exists := c.Get("role") + if !exists || role.(string) != requiredRole { + c.AbortWithStatus(http.StatusForbidden) + return + } c.Next() } } diff --git a/src/internal/server/server.go b/src/internal/server/server.go index 7d024f1..ca29355 100644 --- a/src/internal/server/server.go +++ b/src/internal/server/server.go @@ -92,9 +92,16 @@ func (s *Server) setupRoutes() { cacheGroup.Use(middleware.BasicAuth(s.cfg.Auth.Users)) } + // GET and HEAD are accessible to all authenticated users (reader + writer) cacheGroup.GET("/:key", cacheHandler.Get) - cacheGroup.PUT("/:key", cacheHandler.Put) cacheGroup.HEAD("/:key", cacheHandler.Head) + + // PUT requires the "writer" role + writeGroup := cacheGroup.Group("") + if s.cfg.Auth.Enabled { + writeGroup.Use(middleware.RequireRole("writer")) + } + writeGroup.PUT("/:key", cacheHandler.Put) } // handlePing is a simple health check endpoint. From 3356aeb3031f9b428eaafc50543487e2523e879d Mon Sep 17 00:00:00 2001 From: = Date: Sun, 15 Feb 2026 22:18:51 +0100 Subject: [PATCH 2/3] Improve Cache auth --- chart/templates/auth-secrets.yaml | 27 +++++++++++++++ chart/templates/configmap.yaml | 9 +++-- chart/templates/deployment.yaml | 49 -------------------------- chart/templates/redis-secret.yaml | 18 ++++++++++ chart/templates/secrets.yaml | 57 ------------------------------- src/internal/config/config.go | 42 ++++++----------------- src/internal/middleware/auth.go | 44 +++++------------------- src/internal/server/server.go | 21 ++++++------ 8 files changed, 79 insertions(+), 188 deletions(-) create mode 100644 chart/templates/auth-secrets.yaml create mode 100644 chart/templates/redis-secret.yaml delete mode 100644 chart/templates/secrets.yaml diff --git a/chart/templates/auth-secrets.yaml b/chart/templates/auth-secrets.yaml new file mode 100644 index 0000000..1e7cb85 --- /dev/null +++ b/chart/templates/auth-secrets.yaml @@ -0,0 +1,27 @@ +{{- if .Values.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 }} diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index 6f10e89..bef45c2 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -28,11 +28,10 @@ data: auth: enabled: {{ .Values.auth.enabled }} {{- if .Values.auth.enabled }} - users: - - username: {{ .Values.auth.reader.username | quote }} - role: "reader" - - username: {{ .Values.auth.writer.username | quote }} - role: "writer" + reader: + username: {{ .Values.auth.reader.username | quote }} + writer: + username: {{ .Values.auth.writer.username | quote }} {{- end }} logging: diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml index 25b5e02..b5ee6bb 100644 --- a/chart/templates/deployment.yaml +++ b/chart/templates/deployment.yaml @@ -33,31 +33,7 @@ spec: echo "Redis not ready, waiting..." sleep 2 done -<<<<<<< HEAD - echo "MinIO is ready!" - - name: create-bucket - image: minio/mc:latest - command: - - sh - - -c - - | - mc alias set minio http://{{ .Release.Name }}-minio:9000 $MINIO_ACCESS_KEY $MINIO_SECRET_KEY - mc mb --ignore-existing minio/gradle-cache - echo "Bucket created or already exists" - env: - - name: MINIO_ACCESS_KEY - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-minio-secrets - key: minio-access-key - - name: MINIO_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-minio-secrets - key: minio-secret-key -======= echo "Redis is ready!" ->>>>>>> main containers: - name: cache-server image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -70,44 +46,19 @@ spec: - name: REDIS_PASSWORD valueFrom: secretKeyRef: -<<<<<<< HEAD - name: {{ .Release.Name }}-minio-secrets - key: minio-access-key - - name: MINIO_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-minio-secrets - key: minio-secret-key -======= name: {{ .Release.Name }}-redis-secret key: redis-password ->>>>>>> main {{- if .Values.auth.enabled }} - - name: CACHE_READER_USERNAME - valueFrom: - secretKeyRef: -<<<<<<< HEAD - name: {{ .Release.Name }}-cache-reader - key: username - name: CACHE_READER_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-cache-reader key: password - - name: CACHE_WRITER_USERNAME - valueFrom: - secretKeyRef: - name: {{ .Release.Name }}-cache-writer - key: username - name: CACHE_WRITER_PASSWORD valueFrom: secretKeyRef: name: {{ .Release.Name }}-cache-writer key: password -======= - name: {{ .Release.Name }}-redis-secret - key: cache-password ->>>>>>> main {{- end }} args: - --config diff --git a/chart/templates/redis-secret.yaml b/chart/templates/redis-secret.yaml new file mode 100644 index 0000000..7b62ecd --- /dev/null +++ b/chart/templates/redis-secret.yaml @@ -0,0 +1,18 @@ +{{- if .Values.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-redis-secret + labels: + {{- include "theia-shared-cache.labels" . | nindent 4 }} + app.kubernetes.io/component: storage +type: Opaque +data: + {{- $secretName := printf "%s-redis-secret" .Release.Name }} + {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace $secretName }} + {{- if $existingSecret }} + redis-password: {{ index $existingSecret.data "redis-password" }} + {{- else }} + redis-password: {{ randAlphaNum 32 | b64enc}} + {{- end }} +{{- end }} diff --git a/chart/templates/secrets.yaml b/chart/templates/secrets.yaml deleted file mode 100644 index 91b9480..0000000 --- a/chart/templates/secrets.yaml +++ /dev/null @@ -1,57 +0,0 @@ -{{- if .Values.enabled }} -# MinIO storage credentials -apiVersion: v1 -kind: Secret -metadata: -<<<<<<< HEAD - name: {{ .Release.Name }}-minio-secrets -======= - name: {{ .Release.Name }}-redis-secret ->>>>>>> main - labels: - {{- include "theia-shared-cache.labels" . | nindent 4 }} - app.kubernetes.io/component: storage -type: Opaque -data: -<<<<<<< HEAD - minio-access-key: {{ "minioadmin" | b64enc | quote }} - minio-secret-key: {{ "minioadmin" | b64enc | quote }} -{{- if .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 }} -======= - {{- $secretName := printf "%s-redis-secret" .Release.Name }} - {{- $existingSecret := lookup "v1" "Secret" .Release.Namespace $secretName }} - {{- if $existingSecret }} - redis-password: {{ index $existingSecret.data "redis-password" }} - {{- else }} - redis-password: {{ randAlphaNum 32 | b64enc}} - {{- end }} - cache-password: {{ .Values.auth.password | b64enc | quote}} ->>>>>>> main -{{- end }} diff --git a/src/internal/config/config.go b/src/internal/config/config.go index b831a13..d797ce3 100644 --- a/src/internal/config/config.go +++ b/src/internal/config/config.go @@ -2,7 +2,6 @@ package config import ( "fmt" - "os" "strings" "time" @@ -43,14 +42,14 @@ 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 { Username string `mapstructure:"username"` Password string `mapstructure:"password"` - Role string `mapstructure:"role"` } type MetricsConfig struct { @@ -108,7 +107,9 @@ 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 @@ -116,40 +117,19 @@ func Load(configPath string) (*Config, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } - // Override user credentials from environment variables if set - overrideUserFromEnv(&cfg, 0, "CACHE_READER_USERNAME", "CACHE_READER_PASSWORD") - overrideUserFromEnv(&cfg, 1, "CACHE_WRITER_USERNAME", "CACHE_WRITER_PASSWORD") - return &cfg, nil } -// overrideUserFromEnv overrides username and password for a specific user index -// from environment variables, if they are set. -func overrideUserFromEnv(cfg *Config, index int, usernameEnv, passwordEnv string) { - if index >= len(cfg.Auth.Users) { - return - } - if val := os.Getenv(usernameEnv); val != "" { - cfg.Auth.Users[index].Username = val - } - if val := os.Getenv(passwordEnv); val != "" { - cfg.Auth.Users[index].Password = val - } -} - 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 { diff --git a/src/internal/middleware/auth.go b/src/internal/middleware/auth.go index d2f667e..6d208b8 100644 --- a/src/internal/middleware/auth.go +++ b/src/internal/middleware/auth.go @@ -8,22 +8,9 @@ import ( "github.com/kevingruber/gradle-cache/internal/config" ) -type userCredential struct { - Password string - Role string -} - // BasicAuth creates a middleware that validates HTTP Basic Authentication // and stores the user's role in the gin context. -func BasicAuth(users []config.UserAuth) gin.HandlerFunc { - // Build a map for O(1) lookup - credentials := make(map[string]userCredential, len(users)) - for _, user := range users { - credentials[user.Username] = userCredential{ - Password: user.Password, - Role: user.Role, - } - } +func CacheAuth(auth config.AuthConfig, requireWriter bool) gin.HandlerFunc { return func(c *gin.Context) { username, password, ok := c.Request.BasicAuth() @@ -33,36 +20,23 @@ func BasicAuth(users []config.UserAuth) gin.HandlerFunc { return } - cred, userExists := credentials[username] - if !userExists { - c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) - c.AbortWithStatus(http.StatusUnauthorized) - return - } + // 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 - // Use constant-time comparison to prevent timing attacks - if subtle.ConstantTimeCompare([]byte(password), []byte(cred.Password)) != 1 { + if !isReader && !isWriter { c.Header("WWW-Authenticate", `Basic realm="Gradle Build Cache"`) c.AbortWithStatus(http.StatusUnauthorized) return } - // Store username and role in context for logging/metrics and authorization - c.Set("username", username) - c.Set("role", cred.Role) - c.Next() - } -} - -// RequireRole creates a middleware that checks if the authenticated user -// has the required role. Returns 403 Forbidden if not. -func RequireRole(requiredRole string) gin.HandlerFunc { - return func(c *gin.Context) { - role, exists := c.Get("role") - if !exists || role.(string) != requiredRole { + if requireWriter && !isWriter { c.AbortWithStatus(http.StatusForbidden) return } + c.Next() } } diff --git a/src/internal/server/server.go b/src/internal/server/server.go index ca29355..e92a2f8 100644 --- a/src/internal/server/server.go +++ b/src/internal/server/server.go @@ -88,20 +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)) - } - // GET and HEAD are accessible to all authenticated users (reader + writer) - cacheGroup.GET("/:key", cacheHandler.Get) - 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) +} - // PUT requires the "writer" role - writeGroup := cacheGroup.Group("") - if s.cfg.Auth.Enabled { - writeGroup.Use(middleware.RequireRole("writer")) +func (s *Server) cacheAuth(requireWriter bool) gin.HandlerFunc { + if !s.cfg.Auth.Enabled { + return func(c *gin.Context) { + c.Next() + } } - writeGroup.PUT("/:key", cacheHandler.Put) + return middleware.CacheAuth(s.cfg.Auth, requireWriter) } // handlePing is a simple health check endpoint. From 66fbf2b0361b6b291ccb23ab11ca2b31004b57a5 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 15 Feb 2026 22:32:25 +0100 Subject: [PATCH 3/3] Minor improvements --- chart/templates/auth-secrets.yaml | 2 +- src/internal/middleware/auth.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/chart/templates/auth-secrets.yaml b/chart/templates/auth-secrets.yaml index 1e7cb85..9291768 100644 --- a/chart/templates/auth-secrets.yaml +++ b/chart/templates/auth-secrets.yaml @@ -1,4 +1,4 @@ -{{- if .Values.enabled }} +{{- if and .Values.enabled .Values.auth.enabled }} # Read-only cache credentials (for Theia IDE / students) apiVersion: v1 kind: Secret diff --git a/src/internal/middleware/auth.go b/src/internal/middleware/auth.go index 6d208b8..df86b48 100644 --- a/src/internal/middleware/auth.go +++ b/src/internal/middleware/auth.go @@ -8,8 +8,7 @@ import ( "github.com/kevingruber/gradle-cache/internal/config" ) -// BasicAuth creates a middleware that validates HTTP Basic Authentication -// and stores the user's role in the gin context. +// CacheAuth creates a middleware that validates HTTP Basic Authentication func CacheAuth(auth config.AuthConfig, requireWriter bool) gin.HandlerFunc { return func(c *gin.Context) {