Skip to content

Commit 136dff6

Browse files
committed
feat(cas): add configurable CORS headers to HTTP download endpoint
The platform frontend needs to fetch() content from CAS signed URLs to render evidence inline (SBOMs, SARIF, etc.). Currently browsers block these requests because the CAS HTTP endpoint returns no CORS headers. Add a configurable CORS middleware that sits outside auth so OPTIONS preflights are handled before token validation. Exposed via Helm chart as cas.allowedOrigins (supports both YAML list and comma-separated string). When empty (default), CORS is disabled and the middleware is a passthrough. Closes #2848 Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 22932b1 commit 136dff6

8 files changed

Lines changed: 338 additions & 29 deletions

File tree

app/artifact-cas/configs/config.devel.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ server:
88
# Timeouts for http downloads
99
# grpc downloads/uploads don't require this because they don't have timeouts
1010
timeout: 300s
11+
# cors:
12+
# allow_origins:
13+
# - "http://localhost:3000"
1114
grpc:
1215
addr: 0.0.0.0:9001
1316
# Some unary RPCs are slow, so we need to increase the timeout

app/artifact-cas/internal/conf/conf.pb.go

Lines changed: 83 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/artifact-cas/internal/conf/conf.proto

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -42,6 +42,10 @@ message Server {
4242
string network = 1;
4343
string addr = 2;
4444
google.protobuf.Duration timeout = 3;
45+
CORS cors = 4;
46+
}
47+
message CORS {
48+
repeated string allow_origins = 1;
4549
}
4650
message TLS {
4751
// path to certificate and private key
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// Copyright 2026 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package server
17+
18+
import "net/http"
19+
20+
// CORSMiddleware returns an http.Handler that applies CORS headers based on allowedOrigins.
21+
// If allowedOrigins is empty, the middleware is a passthrough (CORS disabled).
22+
// If "*" is in the list, any origin is allowed.
23+
// Otherwise, only origins in the list are echoed back.
24+
// OPTIONS preflight requests are short-circuited with 204 No Content before reaching the next handler.
25+
func CORSMiddleware(allowedOrigins []string, next http.Handler) http.Handler {
26+
if len(allowedOrigins) == 0 {
27+
return next
28+
}
29+
30+
wildcard := false
31+
allowed := make(map[string]struct{}, len(allowedOrigins))
32+
for _, o := range allowedOrigins {
33+
if o == "*" {
34+
wildcard = true
35+
}
36+
allowed[o] = struct{}{}
37+
}
38+
39+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40+
origin := r.Header.Get("Origin")
41+
if origin == "" {
42+
next.ServeHTTP(w, r)
43+
return
44+
}
45+
46+
var matchedOrigin string
47+
switch {
48+
case wildcard:
49+
matchedOrigin = "*"
50+
default:
51+
if _, ok := allowed[origin]; ok {
52+
matchedOrigin = origin
53+
}
54+
}
55+
56+
if matchedOrigin == "" {
57+
next.ServeHTTP(w, r)
58+
return
59+
}
60+
61+
w.Header().Set("Access-Control-Allow-Origin", matchedOrigin)
62+
if matchedOrigin != "*" {
63+
w.Header().Set("Vary", "Origin")
64+
}
65+
66+
if r.Method == http.MethodOptions {
67+
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
68+
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
69+
w.Header().Set("Access-Control-Max-Age", "86400")
70+
w.WriteHeader(http.StatusNoContent)
71+
return
72+
}
73+
74+
next.ServeHTTP(w, r)
75+
})
76+
}

0 commit comments

Comments
 (0)